安全控制

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的使用

  1. #shiroAuthenticated()
  2. 登陆成功:您的用户名是:#(SESSION("username"))
  3. #end

shiroGuest的使用

  1. #shiroGuest()
  2. 游客您好
  3. #end

shiroHasAllPermission的使用

  1. #shiroHasAllPermission(permissionName1,permissionName2)
  2. 您好,您拥有了权限 permissionName1和permissionName2
  3. #end

shiroHasAllRoles的使用

  1. #shiroHasAllRoles(role1, role2)
  2. 您好,您拥有了角色 role1和role2
  3. #end

shiroHasAnyPermission的使用

  1. #shiroHasAnyPermission(permissionName1,permissionName2)
  2. 您好,您拥有了权限 permissionName1 或 permissionName2
  3. #end

shiroHasAnyRoles的使用

  1. #shiroHasAllRoles(role1, role2)
  2. 您好,您拥有了角色 role1 或 role2
  3. #end

shiroHasPermission的使用

  1. #shiroHasPermission(permissionName1)
  2. 您好,您拥有了权限 permissionName1
  3. #end

shiroHasRole的使用

  1. #shiroHasRole(role1)
  2. 您好,您拥有了角色 role1
  3. #end

shiroNoAuthenticated的使用

  1. #shiroNoAuthenticated()
  2. 您好,您还没有登陆
  3. #end

shiroNotHasPermission的使用

  1. #shiroNotHasPermission(permissionName1)
  2. 您好,您没有权限 permissionName1
  3. #end

shiroNotHasRole的使用

  1. #shiroNotHasRole(role1)
  2. 您好,您没有角色role1
  3. #end

shiroPrincipal的使用

  1. #shiroPrincipal()
  2. 您好,您的登陆信息是:#(principal)
  3. #end

5个Requires注解功能(用在Controller上)

指令 描述
RequiresPermissions 需要权限才能访问这个action
RequiresRoles 需要角色才能访问这个action
RequiresAuthentication 需要授权才能访问这个action,即:SecurityUtils.getSubject().isAuthenticated()
RequiresUser 获取到用户信息才能访问这个action,即:SecurityUtils.getSubject().getPrincipal() != null
RequiresGuest 和RequiresUser相反

RequiresPermissions的使用

  1. public class MyController extends JbootController{
  2. @RequiresPermissions("permission1")
  3. public void index(){
  4. }
  5. @RequiresPermissions(value={"permission1","permission2"},logical=Logincal.AND)
  6. public void index1(){
  7. }
  8. }

RequiresRoles的使用

  1. public class MyController extends JbootController{
  2. @RequiresRoles("role1")
  3. public void index(){
  4. }
  5. @RequiresRoles(value = {"role1","role2"},logical=Logincal.AND)
  6. public void userctener(){
  7. }
  8. }

RequiresUser、RequiresGuest、RequiresAuthentication的使用

  1. public class MyController extends JbootController{
  2. @RequiresUser
  3. public void userCenter(){
  4. }
  5. @RequiresGuest
  6. public void login(){
  7. }
  8. @RequiresAuthentication
  9. public void my(){
  10. }
  11. }

JWT

JWT简介

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT的使用

在server段使用JWT

在Server端使用JWT非常简单,代码如下:

  1. public class JwtController extends JbootController {
  2. public void index() {
  3. setJwtAttr("key1", "test1");
  4. setJwtAttr("key2", "test2");
  5. //do your sth
  6. }
  7. public void show() {
  8. String value = getJwtPara("key1");
  9. // value : test1
  10. }
  11. }

注意: 在Server端使用JWT,必须在jboot.properties配置文件中配置上 jwt 的秘钥,代码如下:

  1. 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,例如:

  1. public class MyshiroListener implements JbootShiroInvokeListener {
  2. private JbootShiroConfig config = Jboot.config(JbootShiroConfig.class);
  3. @Override
  4. public void onInvokeBefore(FixedInvocation inv) {
  5. //do nothing
  6. }
  7. @Override
  8. public void onInvokeAfter(FixedInvocation inv, AuthorizeResult result) {
  9. if (result == null || result.isOk()) {
  10. inv.invoke();
  11. return;
  12. }
  13. int errorCode = result.getErrorCode();
  14. switch (errorCode) {
  15. case AuthorizeResult.ERROR_CODE_UNAUTHENTICATED:
  16. doProcessUnauthenticated(inv.getController());
  17. break;
  18. case AuthorizeResult.ERROR_CODE_UNAUTHORIZATION:
  19. doProcessuUnauthorization(inv.getController());
  20. break;
  21. default:
  22. inv.getController().renderError(404);
  23. }
  24. }
  25. public void doProcessUnauthenticated(Controller controller) {
  26. // 处理认证失败
  27. }
  28. public void doProcessuUnauthorization(Controller controller) {
  29. // 处理授权失败
  30. }
  31. };

其次在jboot.properties中配置即可

  1. jboot.shiro.invokeListener=com.xxx.MyshiroListener

shiro 和 jwt 整合

和自定义shiro错误处理一样。 编写一个类实现 实现接口 io.jboot.component.shiro.JbootShiroInvokeListener,例如:

  1. public class MyshiroListener implements JbootShiroInvokeListener {
  2. @Override
  3. public void onInvokeBefore(FixedInvocation inv) {
  4. String userId = String.valueOf(inv.getController.getJwtPara(USER_ID));
  5. JwtAuthenticationToken token = new JwtAuthenticationToken();
  6. token.setUserId(userId);
  7. token.setToken(userId);
  8. Subject subject = SecurityUtils.getSubject();
  9. subject.login(token);
  10. return subject;
  11. }
  12. @Override
  13. public void onInvokeAfter(FixedInvocation inv, AuthorizeResult result) {
  14. // ....
  15. }
  16. };

同时在jboot.properties中配置即可

  1. jboot.shiro.invokeListener=com.xxx.MyshiroListener

自定义JwtAuthenticationToken

  1. public class JwtAuthenticationToken implements AuthenticationToken {
  2. /** 用户id */
  3. private String userId;
  4. /** token */
  5. private String token;
  6. @Override
  7. public Object getPrincipal() {
  8. return userId;
  9. }
  10. @Override
  11. public Object getCredentials() {
  12. return token;
  13. }
  14. ... getter setter
  15. }

实现shiro realm JwtAuthorizingRealm

  1. public class JwtAuthorizingRealm extends AuthorizingRealm {
  2. @Override
  3. public boolean supports(AuthenticationToken token) {
  4. return token instanceof JwtAuthenticationToken;
  5. }
  6. @Override
  7. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  8. JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) token;
  9. String uid = (String) jwtToken.getPrincipal();
  10. // 此处判断 uid 是否存在,可以访问等操作
  11. return new SimpleAuthenticationInfo(uid, jwtToken.getCredentials(), this.getName());
  12. }
  13. @Override
  14. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  15. // 此处获取 uid 角色权限
  16. return null;
  17. }
  18. }

实现jwt 无状态化,JwtSubjectFactory

  1. public class JwtSubjectFactory extends DefaultWebSubjectFactory {
  2. @Override
  3. public Subject createSubject(SubjectContext context) {
  4. if (context.getAuthenticationToken() instanceof JwtAuthenticationToken) {
  5. // jwt 不创建 session
  6. context.setSessionCreationEnabled(false);
  7. }
  8. return super.createSubject(context);
  9. }
  10. }

jboot.properties中配置

  1. #---------------------------------------------------------------------------------#
  2. jboot.web.jwt.httpHeaderName=Jwt
  3. jboot.web.jwt.secret=xxxxxxxxx
  4. jboot.web.jwt.validityPeriod=1800000
  5. #---------------------------------------------------------------------------------#
  1. shiro.ini中配置
  2. ```xml
  3. [main]
  4. #cache Manager
  5. shiroCacheManager = io.jboot.component.shiro.cache.JbootShiroCacheManager
  6. securityManager.cacheManager = $shiroCacheManager
  7. #realm
  8. dbRealm=xxx.JwtAuthorizingRealm
  9. dbRealm.authorizationCacheName=shiro-authorizationCache
  10. securityManager.realm=$dbRealm
  11. #session manager
  12. sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
  13. sessionManager.sessionValidationSchedulerEnabled=false
  14. #use jwt
  15. subjectFactory=xxx.JwtSubjectFactory
  16. securityManager.subjectFactory=$subjectFactory
  17. securityManager.sessionManager=$sessionManager
  18. #session storage false
  19. securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled=false

认证服务端配置

服务端主要作用为对用户名密码做认证,通过后构建jwt,与正常认证无太大区别,所以下面只给出认证后构建jwt的demo

  1. @RequestMapping("/")
  2. public class MainController extends BaseController {
  3. /**
  4. * 登录 基于 jwt
  5. */
  6. public void postLogin(String loginName, String pwd) {
  7. // 此处判断用户名密码是否正确
  8. String userId = "userId"; //返回用户ID
  9. setJwtAttr("userId", userId); //构建jwt
  10. renderJson(); //返回成功
  11. }
  12. }

shiro 和 sso 整合

和上面介绍的 jwt 的桥接器类似,主要作用是接收 sso 请求,完成客户端应用的局部认证与授权。

以下是一个基于jboot 实现 sso服务端 与 sso客户端的 demo

SSO客户端配置

自定义 SSOAuthenticationToken

  1. public class SSOAuthenticationToken implements AuthenticationToken {
  2. /** 用户id */
  3. private String userId;
  4. /** 全局会话 code */
  5. private String ssoCode;
  6. @Override
  7. public Object getPrincipal() {
  8. return userId;
  9. }
  10. @Override
  11. public Object getCredentials() {
  12. return ssoCode;
  13. }
  14. ... getter setter

实现 JbootShiroInvokeListener 接口:

  1. public class MyshiroListener implements JbootShiroInvokeListener {
  2. @Override
  3. public void onInvokeBefore(FixedInvocation inv) {
  4. String ssoCode = inv.getController().getPara("ssoCode");
  5. String userId = inv.getController().getPara("userId");
  6. if (StringUtils.isBlank(ssoCode) || StringUtils.isBlank(userId)) {
  7. return;
  8. }
  9. SSOAuthenticationToken token = new SSOAuthenticationToken();
  10. token.setUserId(userId);
  11. token.setSsoCode(ssoCode);
  12. try {
  13. Subject subject = SecurityUtils.getSubject();
  14. subject.login(token);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. log.error(e.getMessage());
  18. }
  19. }
  20. }

实现shiro realm SSOAuthorizingRealm

  1. public class SSOAuthorizingRealm extends AuthorizingRealm {
  2. @Override
  3. public boolean supports(AuthenticationToken token) {
  4. return token instanceof SSOAuthenticationToken;
  5. }
  6. @Override
  7. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  8. SSOAuthenticationToken ssoToken = (SSOAuthenticationToken) token;
  9. String uid = (String) ssoToken.getPrincipal();
  10. String ssoCode = token.getCredentials().toString();
  11. //判断ssoCode是否为 sso 系统颁发
  12. // 此处判断 uid 是否存在,可以访问等操作
  13. return new SimpleAuthenticationInfo(uid, ssoToken.getCredentials(), this.getName());
  14. }
  15. @Override
  16. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  17. // 此处获取 uid 角色权限
  18. return null;
  19. }
  20. }

实现 shiro 无认证请求重定向到 sso系统,SSOShiroErrorProcess

  1. public class MyshiroListener implements JbootShiroInvokeListener {
  2. @Override
  3. public void onInvokeAfter(FixedInvocation inv, AuthorizeResult result) {
  4. if (result.isOk()) {
  5. inv.invoke();
  6. return;
  7. }
  8. int errorCode = result.getErrorCode();
  9. switch (errorCode) {
  10. case AuthorizeResult.ERROR_CODE_UNAUTHENTICATED:
  11. doProcessUnauthenticated(inv.getController());
  12. break;
  13. case AuthorizeResult.ERROR_CODE_UNAUTHORIZATION:
  14. doProcessuUnauthorization(inv.getController());
  15. break;
  16. default:
  17. inv.getController().renderError(404);
  18. }
  19. }
  20. public void doProcessUnauthenticated(Controller controller) {
  21. UpmsConfig upmsConfig = Jboot.config(UpmsConfig.class);
  22. StringBuilder ssoServerUrl = new StringBuilder(upmsConfig.getServerUrl());
  23. ssoServerUrl.append("/sso/index").append("?").append("appid").append("=").append(upmsConfig.getAppId()).append("sysid").append("=").append(upmsConfig.getSystemId());
  24. // 回跳地址
  25. StringBuffer backurl = controller.getRequest().getRequestURL();
  26. String queryString = controller.getRequest().getQueryString();
  27. if (StringUtils.isNotBlank(queryString)) {
  28. backurl.append("?").append(queryString);
  29. }
  30. ssoServerUrl.append("&").append("backurl").append("=").append(StringUtils.urlEncode(backurl.toString()));
  31. controller.redirect(ssoServerUrl.toString());
  32. }
  33. public void doProcessuUnauthorization(Controller controller) {
  34. controller.renderError(403);
  35. }
  36. }

shiro.ini中配置

  1. [main]
  2. #cache Manager
  3. shiroCacheManager = io.jboot.component.shiro.cache.JbootShiroCacheManager
  4. securityManager.cacheManager = $shiroCacheManager
  5. #realm
  6. dbRealm=xxx.SSOAuthorizingRealm
  7. dbRealm.authorizationCacheName=shiro-authorizationCache
  8. securityManager.realm=$dbRealm
  9. #session 基于缓存sessionDao,如果缓存已经实现共享,那么session也同样实现共享
  10. sessionDAO=xxx.SessionDAO
  11. sessionDAO.activeSessionsCacheName=shiro-active-session
  12. #设置sessionCookie
  13. sessionIdCookie=org.apache.shiro.web.servlet.SimpleCookie
  14. sessionIdCookie.name=ssotestaid
  15. #sessionIdCookie.domain=demo.com
  16. #sessionIdCookie.path=
  17. #cookie最大有效期,单位秒,默认30天
  18. sessionIdCookie.maxAge=1800
  19. sessionIdCookie.httpOnly=true
  20. #设置session会话管理
  21. sessionManager=org.apache.shiro.web.session.mgt.DefaultWebSessionManager
  22. sessionManager.sessionDAO=$sessionDAO
  23. sessionManager.sessionIdCookie=$sessionIdCookie
  24. sessionManager.sessionIdCookieEnabled=true
  25. sessionManager.sessionIdUrlRewritingEnabled=false
  26. securityManager.sessionManager=$sessionManager
  27. #session过期时间,单位毫秒,默认两天
  28. securityManager.sessionManager.globalSessionTimeout=1800000

SSO服务端配置

SSO服务端,主要包括登录认证、全局code认证、退出等操作。

  1. @RequestMapping("/sso")
  2. @EnableCORS
  3. public class SSOController extends BaseController {
  4. public void index(String appid, String backurl) {
  5. // 判断 appid 是否正确,backurl 是否正确
  6. redirect("/sso/login?backurl=" + StringUtils.urlEncode(backurl));
  7. }
  8. @Before(GET.class)
  9. public void login() {
  10. Subject subject = SecurityUtils.getSubject();
  11. String backurl = getPara("backurl");
  12. if (subject.isAuthenticated()) {
  13. String loginName = (String) subject.getPrincipal();
  14. // 判断用户id
  15. String code = (String) subject.getSession(false).getId().toString();
  16. if (StringUtils.isBlank(backurl)) {
  17. renderJson(JsonResult.buildSuccess(code));
  18. } else {
  19. if (backurl.contains("?")) {
  20. backurl += "&ssoCode=" + code + "&userId=" + upmsUser.getId();
  21. } else {
  22. backurl += "?ssoCode=" + code + "&userId=" + upmsUser.getId();
  23. }
  24. }
  25. redirect(backurl);
  26. } else {
  27. setAttr("backurl", backurl);
  28. render("login.html");
  29. }
  30. }
  31. @Before(POST.class)
  32. @EmptyValidate(value = {
  33. @Form(name = "loginName", message = "用户名不能为空"),
  34. @Form(name = "password", message = "密码不能为空"),
  35. }, renderType = ValidateRenderType.JSON)
  36. public void postLogin(String loginName, String password) {
  37. Subject subject = SecurityUtils.getSubject();
  38. String backUrl = getPara("backUrl", "");
  39. Ret ret = JsonResult.buildSuccess("登录成功", backUrl);
  40. if (!subject.isAuthenticated()) {
  41. UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginName, password);
  42. subject.login(usernamePasswordToken);
  43. // 获取用户 id
  44. Session session = subject.getSession(true);
  45. String code = session.getId().toString();
  46. String backurl = getPara("backurl");
  47. if (StringUtils.isBlank(backurl)) {
  48. renderJson(JsonResult.buildSuccess(code));
  49. } else {
  50. if (backurl.contains("?")) {
  51. backurl += "&ssoCode=" + code + "&userId=" + upmsUser.getId();
  52. } else {
  53. backurl += "?ssoCode=" + code + "&userId=" + upmsUser.getId();
  54. }
  55. }
  56. redirect(backurl);
  57. return;
  58. }
  59. renderJson(ret);
  60. }
  61. @Before(POST.class)
  62. @EmptyValidate(value = {
  63. @Form(name = "code", message = "参数错误"),
  64. }, renderType = ValidateRenderType.JSON)
  65. public void code(String code) {
  66. Object codeCache = null; // 获取缓存全局code
  67. if (codeCache == null) {
  68. renderJson(JsonResult.buildError("invalid"));
  69. } else {
  70. renderJson(JsonResult.buildSuccess("success"));
  71. }
  72. }
  73. public void logout() {
  74. // shiro退出登录
  75. SecurityUtils.getSubject().logout();
  76. // 跳回原地址
  77. String redirectUrl = getRequest().getHeader("Referer");
  78. if (null == redirectUrl) {
  79. redirectUrl = "/";
  80. }
  81. redirect(redirectUrl);
  82. }
  83. }