安全控制
shiro简介
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
shiro的配置
在使用Jboot的shiro模块之前,我假定您已经学习并了解shiro的基础知识。在Jboot中使用shiro非常简单,只需要在resources目录下配置上您的shiro.ini文件即可。在shiro.ini文件里,需要在自行扩展realm等信息。
shiro的使用
Jboot的shiro模块为您提供了以下12个模板指令,同时支持shiro的5个Requires注解功能。方便您使用shiro。
12个模板指令(用在html上)
指令 | 描述 |
---|---|
shiroAuthenticated | 用户已经身份验证通过,Subject.login登录成功 |
shiroGuest | 游客访问时。 但是,当用户登录成功了就不显示了 |
shiroHasAllPermission | 拥有全部权限 |
shiroHasAllRoles | 拥有全部角色 |
shiroHasAnyPermission | 拥有任何一个权限 |
shiroHasAnyRoles | 拥有任何一个角色 |
shiroHasPermission | 有相应权限 |
shiroHasRole | 有相应角色 |
shiroNoAuthenticated | 未进行身份验证时,即没有调用Subject.login进行登录。 |
shiroNotHasPermission | 没有该权限 |
shiroNotHasRole | 没有该角色 |
shiroPrincipal | 获取Subject Principal 身份信息 |
shiroAuthenticated的使用
#shiroAuthenticated()
登陆成功:您的用户名是:#(SESSION("username"))
#end
shiroGuest的使用
#shiroGuest()
游客您好
#end
shiroHasAllPermission的使用
#shiroHasAllPermission(permissionName1,permissionName2)
您好,您拥有了权限 permissionName1和permissionName2
#end
shiroHasAllRoles的使用
#shiroHasAllRoles(role1, role2)
您好,您拥有了角色 role1和role2
#end
shiroHasAnyPermission的使用
#shiroHasAnyPermission(permissionName1,permissionName2)
您好,您拥有了权限 permissionName1 或 permissionName2
#end
shiroHasAnyRoles的使用
#shiroHasAllRoles(role1, role2)
您好,您拥有了角色 role1 或 role2
#end
shiroHasPermission的使用
#shiroHasPermission(permissionName1)
您好,您拥有了权限 permissionName1
#end
shiroHasRole的使用
#shiroHasRole(role1)
您好,您拥有了角色 role1
#end
shiroNoAuthenticated的使用
#shiroNoAuthenticated()
您好,您还没有登陆
#end
shiroNotHasPermission的使用
#shiroNotHasPermission(permissionName1)
您好,您没有权限 permissionName1
#end
shiroNotHasRole的使用
#shiroNotHasRole(role1)
您好,您没有角色role1
#end
shiroPrincipal的使用
#shiroPrincipal()
您好,您的登陆信息是:#(principal)
#end
5个Requires注解功能(用在Controller上)
指令 | 描述 |
---|---|
RequiresPermissions | 需要权限才能访问这个action |
RequiresRoles | 需要角色才能访问这个action |
RequiresAuthentication | 需要授权才能访问这个action,即:SecurityUtils.getSubject().isAuthenticated() |
RequiresUser | 获取到用户信息才能访问这个action,即:SecurityUtils.getSubject().getPrincipal() != null |
RequiresGuest | 和RequiresUser相反 |
RequiresPermissions的使用
public class MyController extends JbootController{
@RequiresPermissions("permission1")
public void index(){
}
@RequiresPermissions(value={"permission1","permission2"},logical=Logincal.AND)
public void index1(){
}
}
RequiresRoles的使用
public class MyController extends JbootController{
@RequiresRoles("role1")
public void index(){
}
@RequiresRoles(value = {"role1","role2"},logical=Logincal.AND)
public void userctener(){
}
}
RequiresUser、RequiresGuest、RequiresAuthentication的使用
public class MyController extends JbootController{
@RequiresUser
public void userCenter(){
}
@RequiresGuest
public void login(){
}
@RequiresAuthentication
public void my(){
}
}
JWT
JWT简介
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT的使用
在server段使用JWT
在Server端使用JWT非常简单,代码如下:
public class JwtController extends JbootController {
public void index() {
setJwtAttr("key1", "test1");
setJwtAttr("key2", "test2");
//do your sth
}
public void show() {
String value = getJwtPara("key1");
// value : test1
}
}
注意: 在Server端使用JWT,必须在jboot.properties配置文件中配置上 jwt 的秘钥,代码如下:
jboot.web.jwt.secret = your_secret
关于JWT的方法:
方法调用 | 描述 |
---|---|
setJwtAttr() | 设置 jwt 的 key 和 value |
setJwtMap() | 把整个 map的key和value 设置到 jwt |
getJwtAttr() | 获取 已经设置进去的 jwt 信息 |
getJwtAttrs() | 获取 所有已经设置进去的 jwt 信息 |
getJwtPara() | 获取客户端传进来的 jwt 信息,若 jwt 超时或者不被信任,那么获取到的内容为null |
在客户端使用JWT
在客户端使用JWT的场景一般是用于非浏览器的第三方进行认证,例如:APP客户端,前后端分离的AJAX请求等。
例如,在登录后,服务器Server会通过 setJwtAttr()
设置上用户数据,客户端可以去获取 HTTP 响应头中的 Jwt,就可以获取 服务器渲染的 Jwt 信息,此时,应该把 Jwt 的信息保存下来,比如保存到 cookie 或 保存在storage等,
在客户每次请求服务器 API 的时候,应该把 Jwt 设置在请求的 http 头中的 Jwt(注意,第一个字母大写),服务器就可以获取到具体是哪个 “用户” 进行请求了。
shiro的其他使用
自定义shiro错误处理
编写一个类实现 实现接口 io.jboot.component.shiro.JbootShiroInvokeListener,例如:
public class MyshiroListener implements JbootShiroInvokeListener {
private JbootShiroConfig config = Jboot.config(JbootShiroConfig.class);
@Override
public void onInvokeBefore(FixedInvocation inv) {
//do nothing
}
@Override
public void onInvokeAfter(FixedInvocation inv, AuthorizeResult result) {
if (result == null || result.isOk()) {
inv.invoke();
return;
}
int errorCode = result.getErrorCode();
switch (errorCode) {
case AuthorizeResult.ERROR_CODE_UNAUTHENTICATED:
doProcessUnauthenticated(inv.getController());
break;
case AuthorizeResult.ERROR_CODE_UNAUTHORIZATION:
doProcessuUnauthorization(inv.getController());
break;
default:
inv.getController().renderError(404);
}
}
public void doProcessUnauthenticated(Controller controller) {
// 处理认证失败
}
public void doProcessuUnauthorization(Controller controller) {
// 处理授权失败
}
};
其次在jboot.properties中配置即可
jboot.shiro.invokeListener=com.xxx.MyshiroListener
shiro 和 jwt 整合
和自定义shiro错误处理一样。 编写一个类实现 实现接口 io.jboot.component.shiro.JbootShiroInvokeListener,例如:
public class MyshiroListener implements JbootShiroInvokeListener {
@Override
public void onInvokeBefore(FixedInvocation inv) {
String userId = String.valueOf(inv.getController.getJwtPara(USER_ID));
JwtAuthenticationToken token = new JwtAuthenticationToken();
token.setUserId(userId);
token.setToken(userId);
Subject subject = SecurityUtils.getSubject();
subject.login(token);
return subject;
}
@Override
public void onInvokeAfter(FixedInvocation inv, AuthorizeResult result) {
// ....
}
};
同时在jboot.properties中配置即可
jboot.shiro.invokeListener=com.xxx.MyshiroListener
自定义JwtAuthenticationToken
public class JwtAuthenticationToken implements AuthenticationToken {
/** 用户id */
private String userId;
/** token */
private String token;
@Override
public Object getPrincipal() {
return userId;
}
@Override
public Object getCredentials() {
return token;
}
... getter setter
}
实现shiro realm JwtAuthorizingRealm
public class JwtAuthorizingRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtAuthenticationToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) token;
String uid = (String) jwtToken.getPrincipal();
// 此处判断 uid 是否存在,可以访问等操作
return new SimpleAuthenticationInfo(uid, jwtToken.getCredentials(), this.getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 此处获取 uid 角色权限
return null;
}
}
实现jwt 无状态化,JwtSubjectFactory
public class JwtSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
if (context.getAuthenticationToken() instanceof JwtAuthenticationToken) {
// jwt 不创建 session
context.setSessionCreationEnabled(false);
}
return super.createSubject(context);
}
}
jboot.properties中配置
#---------------------------------------------------------------------------------#
jboot.web.jwt.httpHeaderName=Jwt
jboot.web.jwt.secret=xxxxxxxxx
jboot.web.jwt.validityPeriod=1800000
#---------------------------------------------------------------------------------#
shiro.ini中配置
```xml
[main]
#cache Manager
shiroCacheManager = io.jboot.component.shiro.cache.JbootShiroCacheManager
securityManager.cacheManager = $shiroCacheManager
#realm
dbRealm=xxx.JwtAuthorizingRealm
dbRealm.authorizationCacheName=shiro-authorizationCache
securityManager.realm=$dbRealm
#session manager
sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
sessionManager.sessionValidationSchedulerEnabled=false
#use jwt
subjectFactory=xxx.JwtSubjectFactory
securityManager.subjectFactory=$subjectFactory
securityManager.sessionManager=$sessionManager
#session storage false
securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled=false
认证服务端配置
服务端主要作用为对用户名密码做认证,通过后构建jwt,与正常认证无太大区别,所以下面只给出认证后构建jwt的demo
@RequestMapping("/")
public class MainController extends BaseController {
/**
* 登录 基于 jwt
*/
public void postLogin(String loginName, String pwd) {
// 此处判断用户名密码是否正确
String userId = "userId"; //返回用户ID
setJwtAttr("userId", userId); //构建jwt
renderJson(); //返回成功
}
}
shiro 和 sso 整合
和上面介绍的 jwt 的桥接器类似,主要作用是接收 sso 请求,完成客户端应用的局部认证与授权。
以下是一个基于jboot 实现 sso服务端 与 sso客户端的 demo
SSO客户端配置
自定义 SSOAuthenticationToken
public class SSOAuthenticationToken implements AuthenticationToken {
/** 用户id */
private String userId;
/** 全局会话 code */
private String ssoCode;
@Override
public Object getPrincipal() {
return userId;
}
@Override
public Object getCredentials() {
return ssoCode;
}
... getter setter
实现 JbootShiroInvokeListener 接口:
public class MyshiroListener implements JbootShiroInvokeListener {
@Override
public void onInvokeBefore(FixedInvocation inv) {
String ssoCode = inv.getController().getPara("ssoCode");
String userId = inv.getController().getPara("userId");
if (StringUtils.isBlank(ssoCode) || StringUtils.isBlank(userId)) {
return;
}
SSOAuthenticationToken token = new SSOAuthenticationToken();
token.setUserId(userId);
token.setSsoCode(ssoCode);
try {
Subject subject = SecurityUtils.getSubject();
subject.login(token);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
}
}
}
实现shiro realm SSOAuthorizingRealm
public class SSOAuthorizingRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof SSOAuthenticationToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
SSOAuthenticationToken ssoToken = (SSOAuthenticationToken) token;
String uid = (String) ssoToken.getPrincipal();
String ssoCode = token.getCredentials().toString();
//判断ssoCode是否为 sso 系统颁发
// 此处判断 uid 是否存在,可以访问等操作
return new SimpleAuthenticationInfo(uid, ssoToken.getCredentials(), this.getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 此处获取 uid 角色权限
return null;
}
}
实现 shiro 无认证请求重定向到 sso系统,SSOShiroErrorProcess
public class MyshiroListener implements JbootShiroInvokeListener {
@Override
public void onInvokeAfter(FixedInvocation inv, AuthorizeResult result) {
if (result.isOk()) {
inv.invoke();
return;
}
int errorCode = result.getErrorCode();
switch (errorCode) {
case AuthorizeResult.ERROR_CODE_UNAUTHENTICATED:
doProcessUnauthenticated(inv.getController());
break;
case AuthorizeResult.ERROR_CODE_UNAUTHORIZATION:
doProcessuUnauthorization(inv.getController());
break;
default:
inv.getController().renderError(404);
}
}
public void doProcessUnauthenticated(Controller controller) {
UpmsConfig upmsConfig = Jboot.config(UpmsConfig.class);
StringBuilder ssoServerUrl = new StringBuilder(upmsConfig.getServerUrl());
ssoServerUrl.append("/sso/index").append("?").append("appid").append("=").append(upmsConfig.getAppId()).append("sysid").append("=").append(upmsConfig.getSystemId());
// 回跳地址
StringBuffer backurl = controller.getRequest().getRequestURL();
String queryString = controller.getRequest().getQueryString();
if (StringUtils.isNotBlank(queryString)) {
backurl.append("?").append(queryString);
}
ssoServerUrl.append("&").append("backurl").append("=").append(StringUtils.urlEncode(backurl.toString()));
controller.redirect(ssoServerUrl.toString());
}
public void doProcessuUnauthorization(Controller controller) {
controller.renderError(403);
}
}
shiro.ini中配置
[main]
#cache Manager
shiroCacheManager = io.jboot.component.shiro.cache.JbootShiroCacheManager
securityManager.cacheManager = $shiroCacheManager
#realm
dbRealm=xxx.SSOAuthorizingRealm
dbRealm.authorizationCacheName=shiro-authorizationCache
securityManager.realm=$dbRealm
#session 基于缓存sessionDao,如果缓存已经实现共享,那么session也同样实现共享
sessionDAO=xxx.SessionDAO
sessionDAO.activeSessionsCacheName=shiro-active-session
#设置sessionCookie
sessionIdCookie=org.apache.shiro.web.servlet.SimpleCookie
sessionIdCookie.name=ssotestaid
#sessionIdCookie.domain=demo.com
#sessionIdCookie.path=
#cookie最大有效期,单位秒,默认30天
sessionIdCookie.maxAge=1800
sessionIdCookie.httpOnly=true
#设置session会话管理
sessionManager=org.apache.shiro.web.session.mgt.DefaultWebSessionManager
sessionManager.sessionDAO=$sessionDAO
sessionManager.sessionIdCookie=$sessionIdCookie
sessionManager.sessionIdCookieEnabled=true
sessionManager.sessionIdUrlRewritingEnabled=false
securityManager.sessionManager=$sessionManager
#session过期时间,单位毫秒,默认两天
securityManager.sessionManager.globalSessionTimeout=1800000
SSO服务端配置
SSO服务端,主要包括登录认证、全局code认证、退出等操作。
@RequestMapping("/sso")
@EnableCORS
public class SSOController extends BaseController {
public void index(String appid, String backurl) {
// 判断 appid 是否正确,backurl 是否正确
redirect("/sso/login?backurl=" + StringUtils.urlEncode(backurl));
}
@Before(GET.class)
public void login() {
Subject subject = SecurityUtils.getSubject();
String backurl = getPara("backurl");
if (subject.isAuthenticated()) {
String loginName = (String) subject.getPrincipal();
// 判断用户id
String code = (String) subject.getSession(false).getId().toString();
if (StringUtils.isBlank(backurl)) {
renderJson(JsonResult.buildSuccess(code));
} else {
if (backurl.contains("?")) {
backurl += "&ssoCode=" + code + "&userId=" + upmsUser.getId();
} else {
backurl += "?ssoCode=" + code + "&userId=" + upmsUser.getId();
}
}
redirect(backurl);
} else {
setAttr("backurl", backurl);
render("login.html");
}
}
@Before(POST.class)
@EmptyValidate(value = {
@Form(name = "loginName", message = "用户名不能为空"),
@Form(name = "password", message = "密码不能为空"),
}, renderType = ValidateRenderType.JSON)
public void postLogin(String loginName, String password) {
Subject subject = SecurityUtils.getSubject();
String backUrl = getPara("backUrl", "");
Ret ret = JsonResult.buildSuccess("登录成功", backUrl);
if (!subject.isAuthenticated()) {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginName, password);
subject.login(usernamePasswordToken);
// 获取用户 id
Session session = subject.getSession(true);
String code = session.getId().toString();
String backurl = getPara("backurl");
if (StringUtils.isBlank(backurl)) {
renderJson(JsonResult.buildSuccess(code));
} else {
if (backurl.contains("?")) {
backurl += "&ssoCode=" + code + "&userId=" + upmsUser.getId();
} else {
backurl += "?ssoCode=" + code + "&userId=" + upmsUser.getId();
}
}
redirect(backurl);
return;
}
renderJson(ret);
}
@Before(POST.class)
@EmptyValidate(value = {
@Form(name = "code", message = "参数错误"),
}, renderType = ValidateRenderType.JSON)
public void code(String code) {
Object codeCache = null; // 获取缓存全局code
if (codeCache == null) {
renderJson(JsonResult.buildError("invalid"));
} else {
renderJson(JsonResult.buildSuccess("success"));
}
}
public void logout() {
// shiro退出登录
SecurityUtils.getSubject().logout();
// 跳回原地址
String redirectUrl = getRequest().getHeader("Referer");
if (null == redirectUrl) {
redirectUrl = "/";
}
redirect(redirectUrl);
}
}