安全控制

    在使用Jboot的shiro模块之前,我假定您已经学习并了解shiro的基础知识。在Jboot中使用shiro非常简单,只需要在resources目录下配置上您的shiro.ini文件即可。在shiro.ini文件里,需要在自行扩展realm等信息。

    Jboot的shiro模块为您提供了以下12个模板指令,同时支持shiro的5个Requires注解功能。方便您使用shiro。

    shiroAuthenticated的使用

    shiroGuest的使用

    1. 游客您好
    2. #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的使用

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

    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. }

    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的方法:

    在客户端使用JWT

    在客户端使用JWT的场景一般是用于非浏览器的第三方进行认证,例如:APP客户端,前后端分离的AJAX请求等。

    编写一个类实现 实现接口 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. public Object getPrincipal() {
    7. return userId;
    8. }
    9. @Override
    10. public Object getCredentials() {
    11. return token;
    12. }
    13. ... getter setter
    14. }

    实现shiro realm JwtAuthorizingRealm

    实现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. }
    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. }

    和上面介绍的 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. break;
    12. case AuthorizeResult.ERROR_CODE_UNAUTHORIZATION:
    13. doProcessuUnauthorization(inv.getController());
    14. break;
    15. default:
    16. inv.getController().renderError(404);
    17. }
    18. }
    19. public void doProcessUnauthenticated(Controller controller) {
    20. UpmsConfig upmsConfig = Jboot.config(UpmsConfig.class);
    21. StringBuilder ssoServerUrl = new StringBuilder(upmsConfig.getServerUrl());
    22. ssoServerUrl.append("/sso/index").append("?").append("appid").append("=").append(upmsConfig.getAppId()).append("sysid").append("=").append(upmsConfig.getSystemId());
    23. // 回跳地址
    24. StringBuffer backurl = controller.getRequest().getRequestURL();
    25. String queryString = controller.getRequest().getQueryString();
    26. if (StringUtils.isNotBlank(queryString)) {
    27. backurl.append("?").append(queryString);
    28. }
    29. ssoServerUrl.append("&").append("backurl").append("=").append(StringUtils.urlEncode(backurl.toString()));
    30. controller.redirect(ssoServerUrl.toString());
    31. }
    32. public void doProcessuUnauthorization(Controller controller) {
    33. controller.renderError(403);
    34. }
    35. }
    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. }